/*
 * ENUM Discoverer. ENUM Demonstration software, demonstrating the potential of ENUM.
 * 
 * Copyright (C) 2009 SIDN and ISOC.nl
 * 
 * This file is part of ENUM Discoverer.
 *
 * ENUM Discoverer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ENUM Discoverer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with ENUM Discoverer.  If not, see <http://www.gnu.org/licenses/>.
 */

package nl.sidn.enumdiscoverer.sync;

import java.util.ArrayList;

import android.database.Cursor;
import android.util.Log;

import nl.sidn.enumdiscoverer.EnumClient;
import nl.sidn.enumdiscoverer.EnumRecord;
import nl.sidn.enumdiscoverer.LogConfig;
import nl.sidn.enumdiscoverer.WebLinkConstants;
import nl.sidn.enumdiscoverer.contacts.ContactLock;
import nl.sidn.enumdiscoverer.contacts.ContactMappingRule;
import nl.sidn.enumdiscoverer.contacts.ContactProfileAggregator;
import nl.sidn.enumdiscoverer.contacts.EnumToContactMapper;
import nl.sidn.enumdiscoverer.contacts.MappingResult;
import nl.sidn.enumdiscoverer.contacts.adapters.ContactAdapter;
import nl.sidn.enumdiscoverer.contacts.adapters.EmailAdapter;
import nl.sidn.enumdiscoverer.contacts.adapters.GeoAdapter;
import nl.sidn.enumdiscoverer.contacts.adapters.HyvesAdapter;
import nl.sidn.enumdiscoverer.contacts.adapters.ImAdapter;
import nl.sidn.enumdiscoverer.contacts.adapters.PhoneNumberAdapter;
import nl.sidn.enumdiscoverer.contacts.adapters.PublicKeyAdapter;
import nl.sidn.enumdiscoverer.contacts.adapters.SIPAdapter;
import nl.sidn.enumdiscoverer.contacts.adapters.WebLinkAdapter;
import nl.sidn.enumdiscoverer.contacts.adapters.VCardAdapter;
import nl.sidn.enumdiscoverer.contacts.adapters.XAddressAdapter;
import nl.sidn.enumdiscoverer.contacts.dao.ContactsDao;
import nl.sidn.enumdiscoverer.contacts.dao.ImProtocols;
import nl.sidn.enumdiscoverer.contacts.dao.PersonDao;

/**
 * Provides methods for synchronising contacts with services in ENUM records
 * @author	Grant Patterson (grant.patterson@catalyst.net.nz)
 */
public class EnumContactsSync {

	static final String TAG = LogConfig.getLogTag(EnumContactsSync.class);
	// whether DEBUG level logging is enabled (whether globally, or explicitly for this log tag)
	static final boolean DEBUG = LogConfig.isDebug(TAG);
	// whether VERBOSE level logging is enabled
	static final boolean VERBOSE = LogConfig.VERBOSE;
	
	/**
	 * Provides the default mapping of ENUM records to ContactAdapters.
	 * Used in EnumContactsSync objects created from the factory method
	 * createWithDefaultMapping()
	 */
	private static final EnumToContactMapper sDefaultContactMapper;
	static {
		ArrayList<ContactMappingRule> rules = new ArrayList<ContactMappingRule>();
		rules.add(new ContactMappingRule("voice:tel", "tel:(\\+[0-9]+)", new PhoneNumberAdapter()));
		rules.add(new ContactMappingRule("pstn:tel", "tel:(\\+[0-9]+)", new PhoneNumberAdapter()));
		rules.add(new ContactMappingRule("sms:tel", "tel:(\\+[0-9]+)", new PhoneNumberAdapter()));
		rules.add(new ContactMappingRule("voice:fax", "tel:(\\+[0-9]+)", new PhoneNumberAdapter()));
		rules.add(new ContactMappingRule("email", "mailto:([^@]+@[^@]+)", new EmailAdapter()));
		
		// Skype instant messaging
		rules.add(new ContactMappingRule("im", "im:([^@]+@skype.*)", new ImAdapter(ImProtocols.SKYPE)));
		// GTalk instant messaging
		rules.add(new ContactMappingRule("im", "im:([^@]+@(:?gtalk)|(:?gmail)|(:?google).*)", new ImAdapter(ImProtocols.GTALK)));
		// Yahoo instant messaging
		rules.add(new ContactMappingRule("im", "im:([^@]+@yahoo.*)", new ImAdapter(ImProtocols.YAHOO)));

		// TODO: AIM instant messaging (untested)
		//rules.add(new ContactMappingRule("im", "im:([^@]+@.*aim.*)", new ImAdapter(ImProtocols.AIM)));

		// TODO: Jabber instant messaging (untested)
		//rules.add(new ContactMappingRule("im", "im:([^@]+@.*jabber.*)", new ImAdapter(ImProtocols.JABBER)));
		
		// TODO: MSN instant messaging (untested)
		//rules.add(new ContactMappingRule("im", "im:([^@]+@.*msn.*)", new ImAdapter(ImProtocols.MSN)));
		
		// TODO: ICQ instant messaging (untested)
		//rules.add(new ContactMappingRule("im", "im:([^@]+@.*icq.*)", new ImAdapter(ImProtocols.ICQ)));
		
		// vCard
		rules.add(new ContactMappingRule("vcard", "http://.+", new VCardAdapter()));

		// x-address
		rules.add(new ContactMappingRule("x-address", "data:(.+)", new XAddressAdapter()));

		// geo: locations URIs
		rules.add(new ContactMappingRule("loc:geo", GeoAdapter.REGEX, new GeoAdapter()));
		
		// public key
		rules.add(new ContactMappingRule("key:http", "http://.+", new PublicKeyAdapter()));
		
		// -- WebLinks -- 
		// Hyves - special case as information is used to populate contact details also.
		rules.add(new ContactMappingRule("web:http", "http://([a-zA-Z0-9\\-]+)\\.hyves\\.nl", new HyvesAdapter()));

		
		// Facebook
		rules.add(new ContactMappingRule("web:http", "http://([^\\.]+)\\.facebook\\.com/.+",
				new WebLinkAdapter(WebLinkConstants.FACEBOOK)));
		// Twitter
		rules.add(new ContactMappingRule("web:http", "http://(?:[^\\.]+\\.)?twitter\\.com/(\\w+)/?",
				new WebLinkAdapter(WebLinkConstants.TWITTER)));
		// Last.fm
		rules.add(new ContactMappingRule("web:http", "http://(?:[^\\.]+\\.)?last\\.fm/user/([^/\\?]+)/?",
				new WebLinkAdapter(WebLinkConstants.LASTFM)));
		// Flickr
		rules.add(new ContactMappingRule("web:http", "http://(?:[^\\.]+\\.)?flickr\\.com(?:/photos)?/([^/\\?]+)/?",
				new WebLinkAdapter(WebLinkConstants.FLICKR)));
		// picasa
		rules.add(new ContactMappingRule("web:http", "http://picasaweb\\.google\\.com/([^/\\?]+)/?",
				new WebLinkAdapter(WebLinkConstants.PICASA)));
		// youtube.com
		rules.add(new ContactMappingRule("web:http", "http://(?:[^\\.]+\\.)?youtube\\.com(?:/user)?/([^/\\?]+)/?",
				new WebLinkAdapter(WebLinkConstants.YOUTUBE)));	
		// LinkedIn - http://eu.linkedin.com/in/xxxxx
		rules.add(new ContactMappingRule("web:http", "http://(?:[^\\.]+\\.)?linkedin\\.com/in/([^/\\?]+)/?",
				new WebLinkAdapter(WebLinkConstants.LINKEDIN)));
		// identi.ca
		rules.add(new ContactMappingRule("web:http", "http://(?:[^\\.]+\\.)?identi\\.ca/([^/\\?]+)/?",
				new WebLinkAdapter(WebLinkConstants.IDENTICA)));	
		// friendfeed.com
		rules.add(new ContactMappingRule("web:http", "http://(?:[^\\.]+\\.)?friendfeed\\.com/([^/\\?]+)",
				new WebLinkAdapter(WebLinkConstants.FRIENDFEED)));
		// plurk
		rules.add(new ContactMappingRule("web:http", "http://(?:[^\\.]+\\.)?plurk\\.com/([^/\\?]+)",
				new WebLinkAdapter(WebLinkConstants.PLURK)));
		// brightkite
		rules.add(new ContactMappingRule("web:http", "http://(?:[^\\.]+\\.)?brightkite\\.com/([^/\\?]+)",
				new WebLinkAdapter(WebLinkConstants.BRIGHTKITE)));
		
		// tumblr
		rules.add(new ContactMappingRule("web:http", "http://([^/\\?]+)tumblr\\.com.*",
				new WebLinkAdapter(WebLinkConstants.TUMBLR)));
		
		// livejournal
		rules.add(new ContactMappingRule("web:http", "http://([^\\.]+)\\.livejournal\\.com.*",
				new WebLinkAdapter(WebLinkConstants.LIVEJOURNAL)));
		
		// dopplr
		rules.add(new ContactMappingRule("web:http", "http://(?:[^\\.]+\\.)?dopplr\\.com/traveller/([^/\\?]+)",
				new WebLinkAdapter(WebLinkConstants.DOPPLR)));
		
		// myspace
		rules.add(new ContactMappingRule("web:http", "http://(?:[^\\.]+\\.)?myspace\\.com/([^/\\?]+)",
				new WebLinkAdapter(WebLinkConstants.MYSPACE)));

		
		// -- SIP Addresses -- 
		rules.add(new ContactMappingRule("sip", "sip:.+", new SIPAdapter()));
		rules.add(new ContactMappingRule("voice:sip", "sip:.+", new SIPAdapter()));
		
		sDefaultContactMapper = new EnumToContactMapper(rules);
	}
	
	private final EnumToContactMapper mContactMapper;
	private final ContactsDao mContactsDao;
	
	/**
	 * Construct an EnumContactsSync
	 * @param contactMapper For mapping ENUM records to the appropriate ContactAdapters
	 * @param contactsDao The DAO to use to access and update Contacts. The caller is responsible
	 *            for closing the ContactsDao.
	 */
	public EnumContactsSync(EnumToContactMapper contactMapper, ContactsDao contactsDao) {
		mContactMapper = contactMapper;
		mContactsDao = contactsDao;
	}
	
	public void syncAllContacts(EnumClient enumClient) {
		
		if (DEBUG) Log.d(TAG, "Synchronising all contacts");
		
		// do lookups for all numbers in contacts, and update local store with
		// any results found.
		// TODO: consider spreading these out so we don't hog the network connection
		
		Cursor c = null;
		
		try {
			
			c = mContactsDao.getAllPhoneNumbers();
			// lookup column indexes here so we don't repeat ourselves
			int numberCol = c.getColumnIndex(ContactsDao.PHONE_NUM_COLUMN);
			int personIdCol = c.getColumnIndex(ContactsDao.PERSON_ID_COLUMN);
			
			while (c.moveToNext()) {
				
				String number = c.getString(numberCol);
				long personId = c.getLong(personIdCol);
				
				ArrayList<EnumRecord> records = enumClient.doEnumLookup(number);
				if (records.isEmpty()) {
					// number wasn't found
					continue;
				}
				
				// sync by personId so we don't need to do a lookup of number
				syncContact(personId, records);
				
			}
		} finally {
			if (c != null) c.close();
		}
	}
	
	/**
	 * Synchronises a contact in Contacts with the services described by enumRecords.
	 * Obtains the ContactLock for personId before modifying the contact and releases it
	 * before returning.
	 * @param personId The personId of the contact to sync
	 * @param enumRecords The ENUM records to use to perform the sync
	 */
	public void syncContact(long personId, ArrayList<EnumRecord> enumRecords) {
		
		if (VERBOSE) Log.d(TAG, "Synchronising contact " + personId);
		
		PersonDao personDao = null;
		ContactLock lock = ContactLock.getApplicationWideLock();
		lock.acquire(personId);
		
		try {
		
			personDao = mContactsDao.getDaoForContact(personId);
			if (personDao == null) {
				Log.i(TAG, "Unable to sync contact. No contact with id " + personId);
				return;
			}
			
			ContactProfileAggregator profileAggregator = new ContactProfileAggregator(personDao);
			syncContact(profileAggregator, enumRecords);
		
		} finally {
			if (personDao != null) personDao.close();
			lock.release(personId);
		}
		
	}
	
	/**
	 * Synchronises a contact in Contacts with the services described by enumRecords.
	 * If profileAggregator writes to the Contacts DB, then the caller should obtain the ContactLock
	 * for the contact before calling this method, and release the ContactLock after this method
	 * returns
	 * @param profileAggregator The ContactProfileAggregator to use in syncing the contact
	 * @param enumRecords The ENUM records to use to perform the sync
	 */
	public void syncContact(ContactProfileAggregator profileAggregator, ArrayList<EnumRecord> enumRecords) {
		
		// Any results that map to Contact fields should be used to update those Contact fields
		int size = enumRecords.size();
		for (int i = 0; i < size; i++) {
			EnumRecord record = enumRecords.get(i);
			String enumService = record.getService();
				
			// get all that Contact adapters matching record
			ArrayList<MappingResult> results =
				mContactMapper.doMapping(enumService, record.getUrl());
			if (results == null) {
				// no matches
				continue;
			}
			
			// update Contacts through each matching adapter
			int rulesSize = results.size();
			for (int j = 0; j < rulesSize; j++) {
				
				MappingResult result = results.get(j);
				ContactAdapter adapter = result.getContactAdapter();

				// catch all exceptions from adapter to prevent application from crashing.
				// Just log and move on to the next adapter
				try {
					adapter.updateContact(result.getMatch(), record, profileAggregator);
				} catch (Exception e) {
					String msg = String.format("Uncaught exception in adapter %s for record %s." +
							" Moving on to next adapter", adapter, record);
					Log.e(TAG, msg, e);
				}
			}
		}
		
		profileAggregator.flush();
	}
	
	
	/**
	 * Creates an EnumContactsSync object using the default ENUM-to-Contacts mapping
	 * @param contactsDao The DAO to use to access and update Contacts. The caller
	 *            is responsible for closing the ContactsDao
	 * @return An EnumContactSync object
	 */
	public static EnumContactsSync createWithDefaultMapping(ContactsDao contactsDao) {
		return new EnumContactsSync(sDefaultContactMapper, contactsDao);
	}
	
}
